/* BDecoder - Converts an InputStream to BEValues.
   Copyright (C) 2003 Mark J. Wielaard

   This file is part of Snark.
   
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.
 
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
 
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

   In addition, as a special exception, the copyright holders of Snark give
   you permission to combine Snark with free software programs or libraries
   that are released under the GNU LGPL and with any code released under
   the Apache Software License, version 1.0, 1.1 or 2.0. You may copy and
   distribute such a system following the terms of the GNU GPL for Snark
   and the following licenses of the other code concerned, provided that
   you include the source code of that other code when and as the GNU GPL
   requires distribution of source code.

   Note that people who make modified versions of Snark are not
   obligated to grant this special exception for their modified versions;
   it is their choice whether to do so. The GNU General Public License
   gives permission to release a modified version without this exception;
   this exception also makes it possible to release a modified version
   which carries forward this exception.
*/

package org.klomp.snark.bencode;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Decodes a bencoded stream to <code>BEValue</code>s.
 *
 * A bencoded byte stream can represent byte arrays, numbers, lists and
 * maps (dictionaries).
 *
 * It currently contains a hack to indicate a name of a dictionary of
 * which a SHA-1 digest hash should be calculated (the hash over the
 * original bencoded bytes).
 *
 * @author Mark Wielaard (mark@klomp.org).
 */
public class BDecoder
{
  // The InputStream to BDecode.
  private final InputStream in;

  // The last indicator read.
  // Zero if unknown.
  // '0'..'9' indicates a byte[].
  // 'i' indicates an Number.
  // 'l' indicates a List.
  // 'd' indicates a Map.
  // 'e' indicates end of Number, List or Map (only used internally).
  // -1 indicates end of stream.
  // Call getNextIndicator to get the current value (will never return zero).
  private int indicator = 0;

  // Used for ugly hack to get SHA hash over the metainfo info map
  private String special_map = "info";
  private boolean in_special_map = false;
  private final MessageDigest sha_digest;

  // Ugly hack. Return the SHA has over bytes that make up the special map.
  public byte[] get_special_map_digest()
  {
    byte[] result = sha_digest.digest();
    return result;
  }

  // Ugly hack. Name defaults to "info".
  public void set_special_map_name(String name)
  {
    special_map = name;
  }

  /**
   * Initalizes a new BDecoder. Nothing is read from the given
   * <code>InputStream</code> yet.
   */
  public BDecoder(InputStream in)
  {
    this.in = in;
    // XXX - Used for ugly hack.
    try
      {
	sha_digest = MessageDigest.getInstance("SHA");
      }
    catch(NoSuchAlgorithmException nsa)
      {
	throw new InternalError(nsa.toString());
      }
  }

  /**
   * Creates a new BDecoder and immediatly decodes the first value it
   * sees.
   *
   * @return The first BEValue on the stream or null when the stream
   * has ended.
   *
   * @exception InvalidBEncoding when the stream doesn't start with a
   * bencoded value or the stream isn't a bencoded stream at all.
   * @exception IOException when somthing bad happens with the stream
   * to read from.
   */
  public static BEValue bdecode(InputStream in) throws IOException
  {
    return new BDecoder(in).bdecode();
  }

  /**
   * Returns what the next bencoded object will be on the stream or -1
   * when the end of stream has been reached. Can return something
   * unexpected (not '0' .. '9', 'i', 'l' or 'd') when the stream
   * isn't bencoded.
   *
   * This might or might not read one extra byte from the stream.
   */
  public int getNextIndicator() throws IOException
  {
    if (indicator == 0)
      {
	indicator = in.read();
	// XXX - Used for ugly hack
	if (in_special_map) sha_digest.update((byte)indicator);
      }
    return indicator;
  }

  /**
   * Gets the next indicator and returns either null when the stream
   * has ended or bdecodes the rest of the stream and returns the
   * appropriate BEValue encoded object.
   */
  public BEValue bdecode() throws IOException
  {
    indicator = getNextIndicator();
    if (indicator == -1)
      return null;

    if (indicator >= '0' && indicator <= '9')
      return bdecodeBytes();
    else if (indicator == 'i')
      return bdecodeNumber();
    else if (indicator == 'l')
      return bdecodeList();
    else if (indicator == 'd')
      return bdecodeMap();
    else
      throw new InvalidBEncodingException
	("Unknown indicator '" + indicator + "'");
  }

  /**
   * Returns the next bencoded value on the stream and makes sure it
   * is a byte array. If it is not a bencoded byte array it will throw
   * InvalidBEncodingException.
   */
  public BEValue bdecodeBytes() throws IOException
  {
    int c = getNextIndicator();
    int num = c - '0';
    if (num < 0 || num > 9)
      throw new InvalidBEncodingException("Number expected, not '"
					  + (char)c + "'");
    indicator = 0;

    c = read();
    int i = c - '0';
    while (i >= 0 && i <= 9)
      {
	// XXX - This can overflow!
	num = num*10 + i;
	c = read();
	i = c - '0';
      }

    if (c != ':')
      throw new InvalidBEncodingException("Colon expected, not '"
					  + (char)c + "'");

    return new BEValue(read(num));
  }

  /**
   * Returns the next bencoded value on the stream and makes sure it
   * is a number. If it is not a number it will throw
   * InvalidBEncodingException.
   */
  public BEValue bdecodeNumber() throws IOException
  {
    int c = getNextIndicator();
    if (c != 'i')
      throw new InvalidBEncodingException("Expected 'i', not '"
					  + (char)c + "'");
    indicator = 0;

    c = read();
    if (c == '0')
      {
	c = read();
	if (c == 'e')
	  return new BEValue(BigInteger.ZERO);
	else
	  throw new InvalidBEncodingException("'e' expected after zero,"
					      + " not '" + (char)c + "'");
      }

    // XXX - We don't support more the 255 char big integers
    char[] chars = new char[256];
    int off = 0;

    if (c == '-')
      {
	c = read();
	if (c == '0')
	  throw new InvalidBEncodingException("Negative zero not allowed");
	chars[off] = (char)c;
	off++;
      }

    if (c < '1' || c > '9')
      throw new InvalidBEncodingException("Invalid Integer start '"
					  + (char)c + "'");
    chars[off] = (char)c;
    off++;

    c = read();
    int i = c - '0';
    while(i >= 0 && i <= 9)
      {
	chars[off] = (char)c;
	off++;
	c = read();
	i = c - '0';
      }

    if (c != 'e')
      throw new InvalidBEncodingException("Integer should end with 'e'");

    String s = new String(chars, 0, off);
    return new BEValue(new BigInteger(s));
  }

  /**
   * Returns the next bencoded value on the stream and makes sure it
   * is a list. If it is not a list it will throw
   * InvalidBEncodingException.
   */
  public BEValue bdecodeList() throws IOException
  {
    int c = getNextIndicator();
    if (c != 'l')
      throw new InvalidBEncodingException("Expected 'l', not '"
					  + (char)c + "'");
    indicator = 0;

    List result = new ArrayList();
    c = getNextIndicator();
    while (c != 'e')
      {
	result.add(bdecode());
	c = getNextIndicator();
      }
    indicator = 0;

    return new BEValue(result);
  }

  /**
   * Returns the next bencoded value on the stream and makes sure it
   * is a map (dictonary). If it is not a map it will throw
   * InvalidBEncodingException.
   */
  public BEValue bdecodeMap() throws IOException
  {
    int c = getNextIndicator();
    if (c != 'd')
      throw new InvalidBEncodingException("Expected 'd', not '"
					  + (char)c + "'");
    indicator = 0;

    Map result = new HashMap();
    c = getNextIndicator();
    while (c != 'e')
      {
	// Dictonary keys are always strings.
	String key = bdecode().getString();

	// XXX ugly hack
	boolean special = special_map.equals(key);
	if (special)
	  in_special_map = true;

	BEValue value = bdecode();
	result.put(key, value);

	// XXX ugly hack continued
	if (special)
	  in_special_map = false;
	
	c = getNextIndicator();
      }
    indicator = 0;

    return new BEValue(result);
  }

  /**
   * Returns the next byte read from the InputStream (as int).
   * Throws EOFException if InputStream.read() returned -1.
   */
  private int read() throws IOException
  {
    int c = in.read();
    if (c == -1)
      throw new EOFException();
    if (in_special_map) sha_digest.update((byte)c);
    return c;
  }

  /**
   * Returns a byte[] containing length valid bytes starting at offset
   * zero.  Throws EOFException if InputStream.read() returned -1
   * before all requested bytes could be read.  Note that the byte[]
   * returned might be bigger then requested but will only contain
   * length valid bytes.  The returned byte[] will be reused when this
   * method is called again.
   */
  private byte[] read(int length) throws IOException
  {
    byte[] result = new byte[length];
    
    int read = 0;
    while (read < length)
      {
	int i = in.read(result, read, length - read);
	if (i == -1)
	  throw new EOFException();
	read += i;
      }

    if (in_special_map) sha_digest.update(result, 0, length);
    return result;
  }

}
